""" sbak/entity/base.py - Implements the basic entity types.
    
    Entities in this module are intended to be used as bases for more
    complex entities classes rather than for creating instances of
    them directly.
"""

import operator

import pygame
from pygame import Rect

from sbak.geometry import Point
from sbak import (
    ID_ANY,
    draw,
    resman, # Needed to make Entity a Representable type
    image,
    event
)

from sbak.event import AbstractEventUser, handler
from sbak.resman import Representable
from sbak.entity import register

import group as group_

__all__ = 'Entity', 'EntityM', 'Actor'

class EntityM(AbstractEventUser.__metaclass__, Representable.__metaclass__):
    """ Metaclass for the Entity class.
    
        Combines the functionality of the metaclasses for
        `event.AbstractEventUser` and `resman.Representable`.
    """
    def __init__(cls, name, bases, dict):
        super(EntityM, cls).__init__(name, bases, dict)
    

class Entity(AbstractEventUser, group_.DynamicSprite, resman.Representable):
    """ Base for physical objects (entities).
    
        ~~~ Constructor Reference ~~~
    
        Entity([world][, pos][, layer][, priority][, id])
    
        world:      The world to place the entity into. This must be a valid
                    world or None (default).
                    
        pos:        Position of entity in world. Pass a point or sequence of
                    two numbers or None (default), which equates to [0,0].
        
        layer:      Determines drawing order in relation to other entities in
                    the world. Defaults to 0.
        
        hidden:     Determines whether or not the entity will be hidden when
                    the world is drawn or displayed. Defaults to False.
        
        priority:   Priority of entity among other entities when receiving
                    events. Defaults to None, which means the entity gets the
                    next highest unused value (lowest priority).
            
        id:         Unique entity ID. Should be a string. Pass None to
                    not give the entity an ID.
        
        ~~~ Description ~~~
    
        An "entity" is a physical object capable of existing in a
        world at a particular position and appearing as a certain
        image whenever a camera displays the world.
        
        Entity is derived from group.DynamicSprite. This means it can
        be added to ordinary pygame.sprite.Group objects by reference,
        but it can also be added to group.DynamicGroup objects by
        group ID.
        
        Although the Entity class is not actually considered a type of
        resource, it is a sub-class of resman.Representable. This makes
        it possible for entities to be represented as parts of worlds.
        Because of this, entity subclasses support the `tag` class
        attribute and the _rep_ and _unrep_ prefixes. (See sbak.resman
        for more details.)
        
        The default rep tag for Entity is 'entity'. This should be overridden
        by subclasses.
        
        ~~~ Attributes ~~~
        
        Attributes marked with * are represented in world files.
        
        id*:        Unique ID string used to refer to the entity.
        
        pos*:       Position of the entity relative to its containing world.
                    Returned as an FPoint. Input values are converted to
                    FPoint as well.
        
        image:      A drawable that is used to represent the entity. When the
                    entity's containing world is drawn or displayed and any
                    visible portion of the entity is in that region, then this
                    image is displayed relative to the entity's current
                    position.
        
        hidden*:    If true, the entity will not be drawn when the
                    containing world is drawn.
                    
        solid:      If true, the entity partakes in collisions. This
                    means that whenever a collision check is performed
                    by something other than the entity, the entity will
                    be included in the list of objects with which the
                    other object could potentially be colliding. If
                    false, the entity becomes "ethereal;" that is,
                    other things will not (normally) collide with it.
        
        layer*:     Controls the order in which entities are drawn in a
                    world. Entities are drawn in order of lowest to
                    highest layer. This MUST be an integer or else
                    errors will occur during processing.
                    
        priority*:  Priority of entity among other entities when receiving
                    events. Entities in a world receive events in order from
                    entities with the lowest `priority` value to highest.
                    
        world:      The world that currently contains the entity. If this is
                    set to None then the entity is not contained by any world.
                    Changing this value will also change the entity's eventhub.
    """
    
    __metaclass__ = EntityM
    
    __branch_names = {"world" : "entity"}
    __primary_parent = "world"
    
    tag = 'entity'
    
    # ~~~ Internal handling methods ~~~
    
    def __init__( self,
                  world = None,
                  pos = None,
                  layer = 0,
                  hidden = False,
                  priority = None,
                  id = None,):
        """ Initializes an Entity object.
            
            Entity.__init__(self)
            Entity.__init__(self[, world][, pos][, layer][, hidden][, priority]
                            [, id])
            
            See `sbak.entity.Entity` for details about the meaning of
            these arguments.
        """

        # Initialize dynamic-sprite components, add to the groups.
        groups = set(groups if groups is not None else [])
        groups.add('entity')
        group_.DynamicSprite.__init__(self, *groups)
        
        # Initialize entity components
        if pos is not None:
            self._pos = FPoint(pos)
        else:
            self._pos = FPoint(0,0)
        self.layer = int(layer) # Determines draw order
        self.hidden = bool(hidden) # Whether or not entity is drawn/displayed
        self.id = id
        self.image = None # Drawable to represent entity
        self._on_kill = [] # Handlers for when the entity dies.
        self.solid = False # Whether or not entity partakes in collisions
        
        # Initialize eventuser components
        AbstractEventUser.__init__(self, priority)
        if world is not None:
            self.world = world
    
    __nonzero__ = group_.DynamicSprite.alive
    
    # ~~~ Represented attributes ~~~
    # Read the docs at sbak.resman.Representable to understand how these work.
    
    _rep_id = operator.attrgetter("id")
    _rep_layer = lambda self: int(self.layer)
    _rep_hidden = lambda self: bool(self.hidden)
    _rep_priority = operator.attrgetter("priority")
    
    def _rep_image(self, m, n):
        return m.res_to_json(self.image, n, True)
    def _unrep_images(self, v, m):
        if isinstance(v, basestring):
            return m.get(m, 'anyimage')
        else:
            return image.from_json(v, m)
    
    _doc_pos =\
        """ Position of the entity in the world as an `FPoint` object. """
    _fget_pos = operator.attrgetter("_pos")
    def _fset_pos(self, pos):
        self._pos = FPoint(pos)
    _rep_pos = operator.attrgetter('pos')
    
    # ~~~ Other methods ~~~
    
    @handler("update")
    def handle_update(self, hook=None, e=None):
        """ Basic update control event handler for entities.
        
            The basic entity updater doesn't do anything except update
            the entity's image if it has one.
            
            If the entity is in a world and not disabled, this method
            will be called for every entity in the world each time
            the world is updated.
            
            The method accepts an optional Event or Hook object (which
            it does not actually use) so that it can be used as an
            event handler for a hook.
            
            This handler is hooked on creation of the entity with the
            default priority.
            
            If this method is overridden, the overriding method should be
            defined as follows:
            
                class MyActor(Actor):
                
                    # ...
                
                    # These three lines are very important.
                    @handler("update")
                    def handle_update(self, hook=None, e=None):
                        super(MyActor, self).handle_update(hook, e)
                        
                        # YOUR CODE HERE
                    
                    # ...
        """
        # Only update the image if there actually is an image and it has
        # an update method (meaning it is not a Surface)
        if self.image is not None and hasattr(self.image, 'update'):
            self.image.update()
    
    def draw(self, dest=None, offset=(0,0), region=None):
        """ Draws the entity's image to a surface.
        
            dest:   Pass the surface onto which the entity should be
                    drawn, or None to draw directly to the screen.
                    
            offset: Pass a sequence of 2 numbers. This offset will be
                    added to the entity's position to get the position
                    of the surface at which the entity will be drawn.
                    (Note: The final coordinates will be converted to
                    integers.)
                    
            region: Pass None, a pygame Rect object, or a compatible
                    object. If the argument is not None, the entity's
                    sprite will only be drawn if the sprite's area in
                    relation to the entity intersects with the area
                    given and it will be drawn with an additional
                    offset of region.topleft.
            
            Worlds will not draw entities if their `hidden` attribute
            is true.
        """        
        # Don't draw if there is no image
        if self.image is not None:
            selfPos = Point(self.pos)
            selfImage = self.image
            
            # Don't use region if it is None
            if region is not None:
                
                # Don't do anything else if sprite is not touching area
                if not selfImage.get_rect(topleft=selfPos).colliderect(region):
                    return
                
                pos = map(sum, zip(selfPos, offset, [-v for v in region[0:2]]))
            
            else:
                pos = map(sum, zip(selfPos, offset))
            
            draw.image(selfImage,dest,pos)
    
    def kill(self):
        """ Kills the entity.
            
            Killing an entity causes the following to occur in order:
            
            1. Execution of all on_kill handlers,
            2. Emptying of on_kill handler list,
            3. Removal of entity from its world,
            4. Removal of entity from all groups.
        """
        [h(self) for h in self.on_kill]
        self._on_kill[:] = []
        Responder.kill(self)
        group_.DynamicSprite.kill(self)
    
    _doc_on_kill =\
        """ Kill pseudo-event handlers.
        
            This will be a list of callables that will be called when
            the entity is killed, after which the list is emptied.
            
            This property is read-only, but the list it refers to is
            not, so handlers may be added like this:
            
            >>> entity.on_kill.append(handler)
            >>> entity.on_kill.append(handler2)
            
            After that, `handler(entity)` will be called followed by
            `handler2(entity)` when the entity is killed.
        """
    _fget_on_kill = operator.attrgetter("_on_kill")
    def _fset_on_kill(self,val):
        raise AttributeError("Read-only attribute. Use "
                             "on_kill.append(h) to add handlers.")

class Actor(Entity):
    """ An entity that is typically interactive with other entities.
    
        ~~~ Constructor ~~~
        
        Actor([world][, pos][, layer][, hidden][, priority][, id][, bbox]
                [, speed])
        
        Actor accepts any of the same arguments that the Entity
        constructor does (in the form of keyword arguments), and
        additionally supports the following paramters:
        
        bbox:       A rectangular area relative to the Actor's
                    position that the Actor occupies. Defaults
                    to (0,0,0,0), meaning that no space is
                    occupied.
        
        speed:      The initial speed of the entity as a point. The default
                    speed is 0,0 (no motion).
                    
        All arguments may be passed by keyword.
    
        ~~~ Description ~~~
    
        This type of entity has the capabilies of speed, gravity,
        and friction in addition to the normal capabilities of an
        entity. It also has additional collision detection methods
        that make it easy to check for collisions with other entities
        in the same world.
        
        ~~~ Attributes ~~~
        
        In addition to the attributes of Entity, Actor objects also have the
        following attributes.
        
        Attributes marked with * are represented in world files.
        
        speed:      The rate at which the actor's pos changes. This is
                    expressed as n FPoint, and is added to the actor's
                    pos every update.
                    
        gravity:    The rate at which the speed changes. This is also expressed
                    as an FPoint, and is added to speed every update.
                    
        friction:   The rate at which the speed approaches 0. This is again an
                    FPoint. It is subtracted from speed every step until speed
                    reaches 0. The effect of negative friction values is
                    undefined.
    """
    
    tag = 'actor'
    
    def __init__( self,
                  world = None,
                  pos = None,
                  layer = 0,
                  hidden = False,
                  priority = None,
                  id = None,
                  bbox = None,
                  speed = None,
                  gravity = None):
        """ Initializes an `Actor` entity.
        
            Actor.__init__(self[, world][, pos][, layer][, hidden][, priority]
                            [, id][, bbox][, speed][, gravity][, friction])
        
            Actor accepts any of the same arguments that the Entity
            constructor does (in the form of keyword arguments), and
            additionally supports the following paramters:
            
            bbox:       A rectangular area relative to the Actor's
                        position that the Actor occupies. Defaults
                        to (0,0,0,0), meaning that no space is
                        occupied.
            
            speed:      The initial speed of the entity as an algebraic
                        (x,y) point. The default speed is 0,0 (no
                        motion).
            
            gravity:    The rate of acceleration as an algebraic
                        (x,y) point. The default is 0,0 (speed
                        remains constant)
            
            friction:   Friction (rate at which speed approaches 0) as
                        an algebraic (x,y) point. The default is
                        0,0 (no friction).
                        
            All arguments may be passed by keyword.
        """
        
        # Pass unhandled attributes to the base constructor
        Entity.__init__(self, world, pos, layer, hidden, priority, id)
        
        # Set default internals
        self._bbox = FRect(bbox)
        self._speed = FPoint(speed)
        self._gravity = FPoint(0,0)
        self._friction = FVecor(0,0)

    @handler("update")
    def handle_update(self,hook=None,e=None):
        """ Updates an actor.
        
            In addition to the basic Entity behavior, this currently
            does the following:
            
            * Adds gravity point to speed point,
            * Adds/subtracts friction point to/from speed point (so speed moves
              towards 0)
            * Adds speed point to coordidante (the entity's position).
            
            If this method is overridden, the overriding method should be
            defined as follows:
            
                class MyActor(Actor):
                
                    # ...
                
                    # These three lines are very important.
                    @handler("update")
                    def handle_update(self, hook=None, e=None):
                        super(MyActor, self).handle_update(hook, e)
                        
                        # YOUR CODE HERE
                    
                    # ...
        """
        super(Actor,self).handle_update(hook, e)
        
        # Localize values for less typing, improved readability,
        # and improved speed
        x, y = self.pos
        xs, ys = self.speed
        xg, yg = self.gravity
        xf, yf = self.friction
        
        # Apply gravity
        xs += xg
        ys += yg
        
        # Apply friction
        if xs > 0:
            xs -= xf
            if xs < 0:
                xs = 0
        elif xs < 0:
            xs += xf
            if xs > 0:
                xs = 0
        if ys > 0:
            ys -= yf
            if ys < 0:
                ys = 0
        elif ys < 0:
            ys += yf
            if ys > 0:
                ys = 0
        
        # Apply speed
        x += xs
        y += ys
        
        # Save changes
        self.pos = x,y
        self.speed = xs,ys
        self.gravity = xg,yg
        self.friction = xf,yf

    _doc_bbox =\
        """ Bounding box as Frect. """
    _fget_bbox = operator.attrgetter("_bbox")
    def _fset_bbox(self, bbox):
        self._bbox = FRect(bbox)

    _doc_region =\
        """ Region occupied as FrozenFRect.
    
            This region is calculated dynamically as a FroenFRect with a
            position that is the sum of the `pos` and the `bbox.pos` of the
            actor and a size that is equal to the `bbox.size` of the actor.
            
            Simply put, this is the `bbox` of the actor offset by the
            `pos` of the vector.
            
            This region is the one used in collision detection calculations
            involving the actor.
    
            Read-only.
        """
    def _fget_region(self):
        return FrozenFRect(self.bbox.pos + self.pos, self.bbox.size)
    
    _doc_left =\
        """ Left edge of occupied region as float. """
    def _fget_left(self):
        return self._pos[0]+self._bbox.left
    def _fset_left(self,L):
        self.pos.x = L-self.bbox.left
    
    # FIXME: These should work as-is, but would work better when updated to work the same way as the property above.
    
    def set_right(self,r):
        self._pos[0] = r-self._bbox.right
    right = property(
        fget = lambda self: self._pos[0]+self._bbox.right,
        fset = set_right,
        doc = " Right edge of occupied region as float. ")
    
    def set_top(self,t):
        self._pos[1] = t-self._bbox.top
    top = property(
        fget = lambda self: self._pos[1]+self._bbox.top,
        fset = set_top,
        doc = " Top edge of occupied region as float. ")
    
    def set_bottom(self,b):
        self._pos[1] = b-self._bbox.bottom
    bottom = property(
        fget = lambda self: self._pos[1]+self._bbox.bottom,
        fset = set_bottom,
        doc = " Bottom edge of occupied region as float. ")
    
    def set_topleft(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.topleft)]
    topleft = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.topleft)),
        fset = set_topleft,
        doc = " Top-left-most point occupied as pair of floats. ")
    
    def set_topright(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.topright)]
    topright = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.topleft)),
        fset = set_topright,
        doc = " Top-right-most point occupied as pair of floats. ")
    
    def set_bottomleft(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.bottomleft)]
    bottomleft = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.bottomleft)),
        fset = set_bottomleft,
        doc = " Bottom-left-most point occupied as pair of floats. ")
    
    def set_bottomright(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.bottomright)]
    bottomright = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.bottomright)),
        fset = set_bottomright,
        doc = " Bottom-right-most point occupied as pair of floats. ")
    
    def collide(self, rect, offset=(0,0)):
        """ Asks the entity if it is colliding with a rectangle.
        
            ~~~ ALIASES ~~~
        
            ent.collide(rect, offset=(0,0)) -> bool
            ent.colliderect(rect, offset=(0,0)) -> bool
        
            ~~~ ARGUMENTS ~~~
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
        
            ~~~ DESCRIPTION ~~~
        
            This is (probably) the simplest collision detection method
            available for an entity. Given a rectangular region, the
            entity compares its own region against the given rectangle.
            If the two rectangles overlap at all, the method returns
            True, otherwise it returns False.
            
            An optional offset may be passed to check if there would be
            a collision with the rectangle if the entity were moved by
            the offset instead of checking where the entity actually is
            at present. This is useful, for example, to see if an
            entity in motion* is about to collide with a rectangle.
            
            * Motion is not provided by Entity; use Actor for that.
        """
        return self.region.move(offset).colliderect(rect)
    
    colliderect = collide
    
    def collide_group(self, rect, group, offset=(0,0)):
        """ Like collide, but checks entity group membership.
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            group:  A pygame.sprite.Group object, or compatible, or
                    a string. If the group is a string, the
                    group.get() function is used to translate it to
                    a group.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
            
            This method provides the same functionality as the
            collide method, but also optionally checks to see
            if the entity is a member of a particular group. If the
            entity is not a member, the method returns False even if
            the entity is colliding with the given rectangle.
            
            For the curious: This method is obviously somewhat
            pointless for 
        """
        if isinstance(group,basestring):
            group = group_.get(group)
        if self not in group:
            return False
        return self.collide(rect, offset)
    
    def collide_groups(self, rect, groups, offset=(0,0)):
        """ Like collide_group, but checks for multiple groups.
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            groups: A sequence of pygame.sprite.Group objects, and/or
                    compatible objects, and/or strings. The check will
                    be false is the entity is not a member of all the
                    groups in the list.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
                    
            This method works the same way as the collide_group
            method, but allows for checking for membership with more
            than one group. If the entity is not a member of all the
            groups, the method returns false even if the entity's
            region is colliding with the rectangle.
        """
        for grp in groups:
            if isinstance(grp,basestring):
                grp = group.get(grp)
            if self not in grp:
                return False
        return self.region.move(offset).colliderect(rect)

    _doc_speed =\
        """ The current speed as an FPoint.
            
            This is the number of pixels the entity will move along the
            x and y axes when UpdateEvent is handled by the default handler.
        """
    _fget_speed = operator.attrgetter("_speed")
    def _fset_speed(self, val):
        self._speed = FPoint(val)
    
    _doc_gravity =\
        """ The current gravity as an FPoint. """
    _fget_gravity = operator.attrgetter("_gravity")
    def _fset_gravity(self, val):
        self._gravity = FPoint(val)
    
    _doc_friction =\
        """ Friction as FPoint.
        
            These are the values that are either added to or subtracted from
            the algebraic x and y speeds until they reach 0.
        """
    _fget_friction = operator.attrgetter("_friction")
    def _fset_friction(self,val):
        self._friction = FPoint(val)
    
    def find_collider(self, offset=(0,0)):
        """ Returns the first entity that is colliding with self.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.world is not None:
            return self.world.find_collider(self.region.move(offset), self)
    
    def find_collider_group(self, group, offset=(0,0)):
        """ Like find_collider, but checks for entity group membership.
        
            group:  A group that the other entity must be a member of.
            
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.world is not None:
            return self.world.find_collider_group(self.region.move(offset),
                                                  self, group)
    
    def find_collider_groups(self, groups, offset=(0,0)):
        """ Like find_collider_group, but checks for multiple groups.
        
            groups: A sequence of groups that the entity must be a
                    member of.
            
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.world is not None:
            return self.world.find_collider_groups(self.region.move(offset),
                                                  self, groups)
    
    def find_colliders(self, offset=(0,0)):
        """ Returns the entities that are colliding with self.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns a list of 0 or more entities.
        """
        if self.world is not None:
            return self.world.find_colliders(self.region.move(offset), self)
        return []
    
    def find_colliders_group(self, group, offset=(0,0)):
        """ Like find_colliders, but checks for entity group membership.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
            
            group:  A group that the other entity must be a member of.
                    
            Returns a list of 0 or more entities.
        """
        if self.world is not None:
            return self.world.find_colliders_group(self.region.move(offset),
                                                   self, group)
    
    def find_colliders_groups(self, groups, offset=(0,0)):
        """ A combination of find_colliders and find_collider_groups.
        
            groups: A sequence of groups that all other entities must
                    be members of.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
            
            Returns a sequence of 0 or more entities.
        """
        if self.world is not None:
            return self.world.find_colliders_groups(self.region.move(offset),
                                                  self, groups)
        return []
